Skip to content

Add Blend v2 integration test suite#664

Open
aditya1702 wants to merge 25 commits into
blend/pr5-graphqlfrom
blend/pr6-integration-tests
Open

Add Blend v2 integration test suite#664
aditya1702 wants to merge 25 commits into
blend/pr5-graphqlfrom
blend/pr6-integration-tests

Conversation

@aditya1702

@aditya1702 aditya1702 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Deploys the full Blend v2 protocol stack (Comet BLND:USDC LP, mock SEP-40 oracle, emitter, pool factory, backstop, one pool with USDC/XLM reserves) on the standalone integration-test network — replicating blend-utils' mock-example.ts deployment order (pinned commit b05242df, wasm provenance + sha256s in infrastructure/testdata/blend/README.md) — then executes real Blend operations and asserts DB state and GraphQL responses through both ingestion paths.

Coverage

  • Operations (11/15 LENDING reasons end-to-end): SUPPLY, WITHDRAW, SUPPLY_COLLATERAL, WITHDRAW_COLLATERAL, BORROW, REPAY, CLAIM (pool + backstop source), LIQUIDATION (oracle-forced undercollateralization → new_auction → 50% fill; both counterparty rows + auction position folds), BACKSTOP_DEPOSIT, BACKSTOP_WITHDRAW_QUEUE, BACKSTOP_WITHDRAW_CANCEL.
  • Read side: reserve rates/APYs vs mock-oracle prices, Comet LP valuation (backstop USD), oracle price snapshots (SEP-40 leg + Comet BLND/LP legs), emissions accrual and claims (claimedBlnd, backstopClaimedLp, token_id = -1 backstop streams), exact net_supplied/net_borrowed cost-basis folds, Q4W JSONB with the ~17-day expiration observed live over GraphQL while queued, and lifetime claimed totals (blend_pool_claimed / blend_backstop_claimed fold positive after the phase-2 claims, with a phase-1 both-empty guard).
  • Ingestion paths: BlendMigrationTestSuite runs protocol-setupprotocol-migrate current-stateprotocol-migrate history (datastore backend, MinIO-fed by the ledger exporter) with cursor-handoff pre/post assertions — the history mode's first automated coverage. BlendLiveIngestionTestSuite restarts ingest with BLEND_PRICE_INTERVAL / BLEND_BACKSTOP_LP_CONTRACT_ID and asserts the full Freighter-facing GraphQL surface (blendPools, blendPool, account.blendPositions, LENDING state changes).

Not covered end-to-end (unit tests only)

  • BACKSTOP_WITHDRAW — the Q4W lockup is 17 days of wall-clock time; ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING only accelerates ledger cadence, not close time, so the lockup cannot elapse on standalone.
  • FLASH_LOAN — needs a custom receiver contract.
  • BAD_DEBT / DEFAULTED_DEBT — require draining the backstop below threshold plus a full liquidation leaving residual debt.

Production changes

  • blndTokenAddress (internal/services/blend/validator.go) gains a standalone-network case: the suite issues BLND from the network master account (keypair.Root of the passphrase), so its SAC address is a fixed constant, letting pool-CLAIM rows carry the real BLND token_id on standalone. Pinned by a unit test and asserted at deploy time in the test setup.
  • canonicalBackstopAddress (same file) gains the matching standalone-network case: the processor folds backstop-shaped entries/events only from the canonical backstop, and on an unpinned network that pin resolves to empty and drops all backstop state (which is what failed this suite — the whale's blend_backstop_positions row never materialized). The suite deploys the backstop from the master account with a fixed salt, so its address is deterministic and pinned the same way: unit test + deploy-time assertion.
  • pkg/wbclient gains Blend query methods (GetBlendPools, GetBlendPool, GetAccountBlendPositions) and LendingChange state-change decoding, with unit tests.

Test infrastructure added

  • Per-actor Soroban executor (executeSorobanOperationAs): arbitrary tx source, RPC-fresh sequences, simulation-provided auth nonces (the existing helper hardcodes nonce 0, safe only for the master account's first use).
  • Salted contract deploys + address precompute, used to break the emitter↔backstop circular dependency by precomputing the backstop's address before deployment.
  • Typed wrappers for every pool/backstop/emitter/oracle/Comet call, ScVal builders with symbol-sorted UDT maps (unit-tested), and named fixture constants so assertions reference the same values the operations used.

Blend phase adds ~100s to make integration-test (total run ~4.5 min, well under the 30m timeout). Verified green twice, including a FORCE_REBUILD=true from-scratch run.

🤖 Generated with Claude Code

@aditya1702
aditya1702 force-pushed the blend/pr6-integration-tests branch from 6f67e5f to 39422d7 Compare July 15, 2026 20:52
@aditya1702
aditya1702 force-pushed the blend/pr6-integration-tests branch from 39422d7 to a98eef2 Compare July 16, 2026 16:56
@aditya1702
aditya1702 force-pushed the blend/pr6-integration-tests branch 2 times, most recently from 8cc6a07 to 7cf40f1 Compare July 21, 2026 18:45
@aditya1702
aditya1702 force-pushed the blend/pr6-integration-tests branch from d67184b to e29e695 Compare July 22, 2026 19:26
@aditya1702
aditya1702 force-pushed the blend/pr6-integration-tests branch from e29e695 to 930f6ad Compare July 22, 2026 19:36
@aditya1702
aditya1702 force-pushed the blend/pr6-integration-tests branch from 930f6ad to 64ce80f Compare July 22, 2026 20:02
…laim math

Claimable BLND was computed against the stored emission index, which is
only as fresh as the stream's last on-chain touch — idle streams
under-reported until someone else interacted. ProjectEmissionIndex
replicates the contract distributors' update_emission_data step
(blend-contracts-v2 @ ba22b487, pool and backstop distributor.rs; mirrored
by blend-sdk-js Emissions.accrue): index + floor(min(now,expiration)-
last_time)*eps*scalar/supply with the contract's exact guards. Pool
streams project over the side's raw b/dToken supply at 10^decimals;
the backstop projects over unqueued shares (shares - q4w) at 1e7.
Contract-test vectors (test_update_emission_data_past_exp/_rounds_down)
pinned bit-exact in rates_test.go.
emissionsAPRFor call sites passed the stored BRate/DRate while every
sibling token/USD figure on the same types is valued at the projected
PB/PD, so emissions APRs were overstated after idle accrual. All
surviving call sites (position reserve, pool catalog) now pass PB/PD.
…d-sdk-js

BlendPoolPosition.netApy divided the weighted APY spread by the net
position value (supplied - borrowed), which diverges from blend-sdk-js's
PositionsEstimate — the convention the Blend UI displays — and explodes
as a leveraged position's net value approaches 0. Now
(Σ suppliedUsd·supplyApy − Σ borrowedUsd·borrowApy) / Σ suppliedUsd, with
the SDK's supply-less rule: exactly 0 (debt is forgiven as bad debt).
…eams

Blend runs two independent emission streams per reserve; the single
emissionsApr picked the bToken side whenever supply/collateral existed,
silently dropping the borrow-stream APR for both-sides positions.
BlendReservePosition now mirrors BlendReserve with emissionsSupplyApr +
emissionsBorrowApr, each the stream's pool-wide APR at projected rates.
Earn discovery is a downstream composition concern: blendPools already
carries every field the asset-first view derived (status gating,
reserve enabled, supplyApy, emissionsSupplyApr, suppliedUsd ordering,
token metadata), so consumers assemble it themselves. Drops the query,
its BlendEarnOption/BlendEarnPoolOption types, resolver, complexity
entry, and the now-unused ReserveModel.GetAll reader.
…te accrual

Adversarial audit vs blend-contracts-v2 @ ba22b487 found two edge-state
deviations in ProjectRates:

- util==0: the contract bumps last_time and skips the rate update when
  nobody borrows; the projection grew dRate by irMod*rBase. Masked today
  (every pD consumer multiplies a zero dToken amount) but a landmine.
- bad debt (liabilities >= supply): the contract grows bRate by
  accrued*(1-bstop)/totalSupply — the UNCLAMPED liabilities/supply
  ratio — while the reduction reused the clamped utilization, so
  suppliers' bRate growth was understated (~0.5% over 30 idle days).

ProjectRates now takes bSupply/dSupply, derives the raw ratio itself,
and mirrors the contract's on-load guards (zero bSupply / zero
liabilities). New vectors: util==0, zero bSupply, bad-debt raw ratio.
…ry/Account

BlendPool.status becomes a BlendPoolStatus enum (ADMIN_ACTIVE..SETUP,
matching the BlendAuctionType precedent); null until ingested and for
any unrecognized on-chain value. The blend entry points move off
'extend type' blocks into the base Query (queries.graphqls) and Account
(account.graphqls) definitions so each type reads complete in one place;
blend.graphqls keeps all Blend object/enum types.
Nested blend lists were priced once under a single outer multiplier, so
a full blendPools selection cost ~29 units per pool instead of pools x
reserves. Every list level now carries its own bound: catalog x50,
per-account pools/backstop/activeAuctions x10 (documented assumption),
reserves and auction bid/lot x30 (MAX_RESERVES), q4w x20 (MAX_Q4W_SIZE);
Account.blendPositions drops back to default pricing so cardinality is
not double-charged. Full-selection worst cases are now blendPools=26150
and blendPositions=7572 - above the current 6000 deployment limit by
design; admitting full selections needs a deployment-side limit raise.
Existing non-blend entries (freighter budget) untouched.
…ct guard

The distributor advances the stream index before any user write and
traps on a negative delta via require_nonnegative; off-chain a negative
delta can only mean an ingestion gap between the stream row and the
user row. ClaimableEmissions now returns the accrued balance alone in
that state instead of a negative claimable.
GetBackstopLPPrices inferred the protocol-wide BLND/LP price group from
a self-priced-row join, but a self-priced row does not uniquely identify
the configured Comet: pools are permissionless, so any pool whose oracle
is also one of its reserve assets produces the same shape, and a stale
formerly-configured Comet's rows linger until they age out.
BLEND_BACKSTOP_LP_CONTRACT_ID - the same pin the snapshot writer
targets - now flows into serve (cmd/serve.go -> Configs -> resolver) and
scopes the query to exactly that oracle; findBackstopPrices reduces to
picking the self-priced LP row and its BLND sibling within the pinned
group. An unset pin returns no rows, so backstop USD fields resolve to
null. Deploy note: the api deployment needs BLEND_BACKSTOP_LP_CONTRACT_ID
in its env (ingest already has it).
…ping, backstop-pool relationship

- interestEarned/interestPaid: lifetime cost-basis semantics on the
  schema (token-denominated, survives full exit, liquidation fills
  adjust the cost basis, may go slightly negative).
- LendingChange: per-reason population table for tokenId/amount/poolId,
  verified against the event decoder (events.go) - which reasons carry
  the reserve asset vs the BLND SAC vs null, share- vs LP- vs
  underlying-denominated amounts, and the backstop CLAIM's null poolId.
- BlendBackstopPosition: the backstop is one protocol-wide contract
  holding per-pool first-loss capital - one entry per backed pool.
- buildBackstopPosition godoc: un-ingested backstop pool row displays
  lpTokens 0 (under-reports, never over-reports).
- Utilization godoc: the contract descales with ceil/floor then ceils
  the 7-decimal ratio; the exact rational here is <= that, by <1e-7 for
  funded reserves.
…or tables

Read blend_pool_claimed/blend_backstop_claimed via the new GetByAccount readers
after phase-2 live ingestion and assert the supplier's pool claim and whale's
backstop claim folded positive totals. Guard that phase-1 (no claims) leaves both
accumulator tables empty.
…s survive

The canonical-backstop pin resolves to empty on the standalone network,
which made the processor drop every backstop-shaped entry and event as a
non-canonical impostor — BlendMigrationTestSuite failed on the whale's
missing blend_backstop_positions row.

The suite deploys the backstop from the master account (keypair.Root of
the passphrase) with a fixed salt, so its address is a deterministic
function of the passphrase alone. Pin it in canonicalBackstopAddress the
same way blndTokenAddress pins the standalone BLND SAC: unit test + a
deploy-time assertion in SetupBlendStack.
Absorbs the review-round API changes on blend/pr5-graphql: drops the
GetBlendEarnOptions client method, query, and types (the query was
removed - earn discovery composes from blendPools); BlendPool.status
becomes the BlendPoolStatus enum string; BlendReservePosition's single
emissionsApr splits into emissionsSupplyApr/emissionsBorrowApr.
Claimable-emission assertions were already sign-based, so the
projected-to-now claimable math needs no test changes.
@aditya1702
aditya1702 force-pushed the blend/pr6-integration-tests branch from 64ce80f to 81c3229 Compare July 22, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants